1.6. Structs
数组和结构是 C 支持创建数据元素集合的两种方式。数组用于创建相同类型的数据元素的有序集合,而结构用于创建不同类型的数据元素的集合。 C 程序员可以通过多种不同的方式组合数组和结构构建块来创建更复杂的数据类型和结构。本节介绍结构体,在下一章中我们将更详细地描述结构体的特征,并展示如何将它们与数组结合起来。
C 不是面向对象的语言;因此,它不支持类。但是,它确实支持定义结构化类型,这就像类的数据部分。结构体是一种用于表示异构数据集合的类型;它是一种将一组不同类型视为单个连贯单元的机制。 C 结构体在各个数据值之上提供了一个抽象级别,将它们视为单一类型。例如,学生有姓名、年龄、平均绩点 (GPA) 和毕业年份。程序员可以定义一个新的结构类型,将这四个数据元素组合成一个结构学生变量,该变量包含姓名值(类型 char [],用于保存字符串)、年龄值(类型 int)、GPA 值(类型float)和毕业年份值(int 类型)。该结构类型的单个变量可以存储特定学生的所有四部分数据;例如(“Freya”, 19, 3.7, 2021)。
在 C 程序中定义和使用结构体类型分为三个步骤:
- 定义表示结构的新结构类型。
- 声明新结构类型的变量。
- 使用点 (.) 表示法访问变量的各个字段值。
1.6.1. 定义结构体类型
结构类型定义应出现在任何函数之外,通常位于程序 .c 文件顶部附近。定义新结构类型的语法如下(struct 是保留关键字):
struct <struct_name> {
<field 1 type> <field 1 name>;
<field 2 type> <field 2 name>;
<field 3 type> <field 3 name>;
...
};
下面是定义新的StudentT 结构体类型用于存储学生数据的示例:
struct studentT {
char name[64];
int age;
float gpa;
int grad_yr;
};
这个结构体定义向C的类型系统添加了一个新类型,该类型的名称是struct StudentT。该结构体定义了四个字段,每个字段定义包括字段的类型和名称。请注意,在此示例中,名称字段的类型是字符数组,用作字符串。
1.6.2. 声明结构类型的变量
定义类型后,您可以声明新类型 struct StudentT 的变量。请注意,与我们迄今为止遇到的仅由一个单词(例如 int、char 和 float)组成的其他类型不同,我们的新结构类型的名称是两个单词:struct StudentT。
struct studentT student1, student2; // student1, student2 are struct studentT
1.6.3. 访问字段值
要访问结构体变量中的字段值,请使用点表示法:
<variable name>.<field name>
访问结构体及其字段时,请仔细考虑您正在使用的变量的类型。新手 C 程序员经常会因为没有考虑结构体字段的类型而在他们的程序中引入错误。表 1 显示了围绕我们的 struct StudentT 类型的几个表达式的类型。
Table 1. 与各种结构体 StudentT 表达式相关的类型
Expression | C type |
---|---|
student1 | struct studentT |
student1.age | integer (int ) |
student1.name | array of characters (char [] ) |
student1.name[3] | character (char ), the type stored in each position of the name array |
以下是分配 struct StudentT 变量字段的一些示例:
// The 'name' field is an array of characters, so we can use the 'strcpy'
// string library function to fill in the array with a string value.
strcpy(student1.name, "Kwame Salter");
// The 'age' field is an integer.
student1.age = 18 + 2;
// The 'gpa' field is a float.
student1.gpa = 3.5;
// The 'grad_yr' field is an int
student1.grad_yr = 2020;
student2.grad_yr = student1.grad_yr;
图 1 说明了在上一示例中进行字段赋值后,student1 变量在内存中的布局。仅结构变量的字段(框中的区域)存储在内存中。为了清楚起见,字段名称在图中进行了标记,但对于 C 编译器来说,字段只是存储位置或距结构变量内存开头的偏移量。例如,根据 struct StudentT 的定义,编译器知道要访问名为 gpa 的字段,它必须跳过由 64 个字符(姓名)和一个整数(年龄)组成的数组。请注意,在图中,名称字段仅描述 64 字符数组的前 6 个字符。
Figure 1. The student1 variable’s memory after assigning each of its fields
C 结构类型是左值,这意味着它们可以出现在赋值语句的左侧。因此,可以使用简单的赋值语句将一个结构变量分配给另一个结构变量的值。赋值语句右侧结构体的字段值被复制到赋值语句左侧结构体的字段值。换句话说,一个结构体的内存内容被复制到另一个结构体的内存中。下面是一个以这种方式分配结构体值的示例:
student2 = student1; // student2 gets the value of student1
// (student1's field values are copied to
// corresponding field values of student2)
strcpy(student2.name, "Frances Allen"); // change one field value
图 2 显示了执行赋值语句和调用 strcpy 后两个 Student 变量的值。请注意,该图将名称字段描述为它们包含的字符串值,而不是 64 个字符的完整数组。
Figure 2. Layout of the student1 and student2 structs after executing the struct assignment and strcpy call
C 提供了一个 sizeof 运算符,它接受一个类型并返回该类型使用的字节数。 sizeof 运算符可用于任何 C 类型(包括结构类型),以查看该类型的变量需要多少内存空间。例如,我们可以打印 struct StudentT 类型的大小:
// Note: the `%lu` format placeholder specifies an unsigned long value.
printf("number of bytes in student struct: %lu\n", sizeof(struct studentT));
运行时,此行应打印出至少 76 个字节的值,因为名称数组中有 64 个字符(每个字符 1 个字节),int Age 字段为 4 个字节,float gpa 字段为 4 个字节,int grad_yr 字段4 个字节。在某些计算机上,确切的字节数可能大于 76。
这是一个完整的示例程序,定义并演示了 struct StudentT 类型的用法:
#include <stdio.h>
#include <string.h>
// Define a new type: struct studentT
// Note that struct definitions should be outside function bodies.
struct studentT {
char name[64];
int age;
float gpa;
int grad_yr;
};
int main(void) {
struct studentT student1, student2;
strcpy(student1.name, "Kwame Salter"); // name field is a char array
student1.age = 18 + 2; // age field is an int
student1.gpa = 3.5; // gpa field is a float
student1.grad_yr = 2020; // grad_yr field is an int
/* Note: printf doesn't have a format placeholder for printing a
* struct studentT (a type we defined). Instead, we'll need to
* individually pass each field to printf. */
printf("name: %s age: %d gpa: %g, year: %d\n",
student1.name, student1.age, student1.gpa, student1.grad_yr);
/* Copy all the field values of student1 into student2. */
student2 = student1;
/* Make a few changes to the student2 variable. */
strcpy(student2.name, "Frances Allen");
student2.grad_yr = student1.grad_yr + 1;
/* Print the fields of student2. */
printf("name: %s age: %d gpa: %g, year: %d\n",
student2.name, student2.age, student2.gpa, student2.grad_yr);
/* Print the size of the struct studentT type. */
printf("number of bytes in student struct: %lu\n", sizeof(struct studentT));
return 0;
}
运行时,该程序输出以下内容:
name: Kwame Salter age: 20 gpa: 3.5, year: 2020
name: Frances Allen age: 20 gpa: 3.5, year: 2021
number of bytes in student struct: 76
左值
左值是可以出现在赋值语句左侧的表达式。它是一个表示内存存储位置的表达式。当我们介绍 C 指针类型以及创建组合 C 数组、结构体和指针的更复杂结构的示例时,仔细考虑类型并记住哪些 C 表达式是有效的左值(可以在左侧使用)非常重要的赋值语句)
从目前我们对 C 的了解来看,基本类型、数组元素和结构体的单个变量都是左值。静态声明的数组的名称不是左值(您无法更改内存中静态声明的数组的基地址)。以下示例代码片段根据不同类型的左值状态说明了有效和无效的 C 赋值语句:
struct studentT {
char name[32];
int age;
float gpa;
int grad_yr;
};
int main(void) {
struct studentT student1, student2;
int x;
char arr[10], ch;
x = 10; // Valid C: x is an lvalue;
ch = 'm'; // Valid C: ch is an lvalue;
student1.age = 18; // Valid C: age field is an lvalue;
student2 = student1; // Valid C: student2 is an lvalue
arr[3] = ch; // Valid C: arr[3] is an lvalue
x + 1 = 8; // Invalid C: x+1 is not an lvalue
arr = "hello"; // Invalid C: arr is not an lvalue
// cannot change base addr of statically declared array
// (use strcpy to copy the string value "hello" to arr)
student1.name = student2.name;
// Invalid C: name field is not an lvalue
// (the base address of a statically
// declared array cannot be changed)
}
</div>
^8704c9
### 1.6.4. 将结构传递给函数
在 C 中,所有类型的参数都按值传递给函数。因此,如果函数具有结构类型参数,那么当使用结构参数调用时,参数的值将传递给其形参,这意味着形参获得其形参值的副本。结构体变量的值是其内存的内容,这就是为什么我们可以在单个赋值语句中将一个结构体的字段分配为与另一个结构体相同的字段,如下所示:
```c
student2 = student1;
由于结构体变量的值代表其内存的全部内容,因此将结构体作为参数传递给函数会为参数提供所有参数结构体字段值的副本。如果函数更改结构体参数的字段值,则参数字段值的更改不会影响参数的相应字段值。也就是说,对参数字段的更改只会修改这些字段的参数内存位置中的值,而不是这些字段的参数内存位置中的值。
这是一个使用带有结构参数的 checkID 函数的完整示例程序:
#include <stdio.h>
#include <string.h>
/* struct type definition: */
struct studentT {
char name[64];
int age;
float gpa;
int grad_yr;
};
/* function prototype (prototype: a declaration of the
* checkID function so that main can call it, its full
* definition is listed after main function in the file):
*/
int checkID(struct studentT s1, int min_age);
int main(void) {
int can_vote;
struct studentT student;
strcpy(student.name, "Ruth");
student.age = 17;
student.gpa = 3.5;
student.grad_yr = 2021;
can_vote = checkID(student, 18);
if (can_vote) {
printf("%s is %d years old and can vote.\n",
student.name, student.age);
} else {
printf("%s is only %d years old and cannot vote.\n",
student.name, student.age);
}
return 0;
}
/* check if a student is at least the min age
* s: a student
* min_age: a minimum age value to test
* returns: 1 if the student is min_age or older, 0 otherwise
*/
int checkID(struct studentT s, int min_age) {
int ret = 1; // initialize the return value to 1 (true)
if (s.age < min_age) {
ret = 0; // update the return value to 0 (false)
// let's try changing the student's age
s.age = min_age + 1;
}
printf("%s is %d years old\n", s.name, s.age);
return ret;
}
当 main 调用 checkID 时,student 结构体的值(其所有字段的内存内容的副本)将传递给 s 参数。当函数更改其参数的年龄字段的值时,它不会影响其参数(学生)的年龄字段。通过运行该程序可以看到此行为,该程序输出以下内容:
Ruth is 19 years old
Ruth is only 17 years old and cannot vote.
输出显示,当 checkID 打印年龄字段时,它反映了函数对参数 s 的年龄字段的更改。但是,在函数调用返回后,main 会打印学生的年龄字段,其值与 checkID 调用之前的值相同。图 3 显示了 checkID 函数返回之前调用堆栈的内容。
Figure 3. The contents of the call stack before returning from the checkID function
当结构体包含静态声明的数组字段(如结构体 StudentT 中的 name 字段)时,理解结构体参数的按值传递语义尤其重要。当这样的结构体传递给函数时,结构体参数的整个内存内容(包括数组字段中的每个数组元素)都会复制到其参数中。如果函数更改了参数结构体的数组内容,则这些更改在函数返回后将不会保留。考虑到我们对数组如何传递给函数的了解,这种行为可能看起来很奇怪,但它与前面描述的结构复制行为是一致的。